package com.yupi.yupao.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.yupi.yupao.common.ErrorCode;
import com.yupi.yupao.exception.BusinessException;
import com.yupi.yupao.model.domain.User;
import com.yupi.yupao.service.UserService;
import com.yupi.yupao.mapper.UserMapper;
import com.yupi.yupao.utils.AlgorithmUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.math3.util.Pair;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.DigestUtils;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.yupi.yupao.contant.UserConstant.ADMIN_ROLE;
import static com.yupi.yupao.contant.UserConstant.USER_LOGIN_STATE;

/**
 * 用户服务实现类
 */
@SuppressWarnings({"all"})
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
        implements UserService {

    @Resource
    private UserMapper userMapper;


    /**
     * 盐值，混淆密码
     */
    private static final String SALT = "yupi";

    /**
     * 用户注册
     *
     * @param userAccount   用户账户
     * @param userPassword  用户密码
     * @param checkPassword 校验密码
     * @param planetCode    星球编号
     * @return 新用户 id
     */
    @Override
    public long userRegister(String userAccount, String userPassword, String checkPassword, String planetCode) {
        // 1. 校验
        if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword, planetCode)) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数为空");
        }
        if (userAccount.length() < 4) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户账号过短");
        }
        if (userPassword.length() < 8 || checkPassword.length() < 8) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户密码过短");
        }
        if (planetCode.length() > 5) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "星球编号过长");
        }
        // 账户不能包含特殊字符
        String validPattern = "[`~!@#$%^&*()+=|{}':;',\\\\[\\\\].<>/?~！@#￥%……&*（）——+|{}【】‘；：”“’。，、？]";
        Matcher matcher = Pattern.compile(validPattern).matcher(userAccount);
        if (matcher.find()) {
            return -1;
        }
        // 密码和校验密码相同
        if (!userPassword.equals(checkPassword)) {
            return -1;
        }
        // 账户不能重复
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("userAccount", userAccount);
        long count = userMapper.selectCount(queryWrapper);
        if (count > 0) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号重复");
        }
        // 星球编号不能重复
        queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("planetCode", planetCode);
        count = userMapper.selectCount(queryWrapper);
        if (count > 0) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "编号重复");
        }
        // 2. 加密
        String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
        // 3. 插入数据
        User user = new User();
        user.setUserAccount(userAccount);
        user.setUserPassword(encryptPassword);
        user.setPlanetCode(planetCode);
        boolean saveResult = this.save(user);
        if (!saveResult) {
            return -1;
        }
        return user.getId();
    }


    /**
     * 用户登录
     *
     * @param userAccount  用户账户
     * @param userPassword 用户密码
     * @param request
     * @return 脱敏后的用户信息
     */
    @Override
    public User userLogin(String userAccount, String userPassword, HttpServletRequest request) {
        // 1. 校验
        if (StringUtils.isAnyBlank(userAccount, userPassword)) {
            return null;
        }
        if (userAccount.length() < 4) {
            return null;
        }
        if (userPassword.length() < 8) {
            return null;
        }
        // 账户不能包含特殊字符
        String validPattern = "[`~!@#$%^&*()+=|{}':;',\\\\[\\\\].<>/?~！@#￥%……&*（）——+|{}【】‘；：”“’。，、？]";
        Matcher matcher = Pattern.compile(validPattern).matcher(userAccount);
        if (matcher.find()) {
            return null;
        }
        // 2. 加密
        String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
        // 查询用户是否存在
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("userAccount", userAccount);
        queryWrapper.eq("userPassword", encryptPassword);
        User user = userMapper.selectOne(queryWrapper);
        // 用户不存在
        if (user == null) {
            log.info("user login failed, userAccount cannot match userPassword");
            return null;
        }
        // 3. 用户脱敏
        User safetyUser = getSafetyUser(user);
        // 4. 记录用户的登录态
        request.getSession().setAttribute(USER_LOGIN_STATE, safetyUser);
        return safetyUser;
    }

    /**
     * 用户脱敏 这里功能还不是比较完善的 严格意义上用户脱敏不能暴露用户的每个字段的全部信息 需要打码
     *
     * @param originUser
     * @return
     */
    @Override
    public User getSafetyUser(User originUser) {
        if (originUser == null) {
            return null;
        }
        User safetyUser = new User();
        safetyUser.setId(originUser.getId());
        safetyUser.setUsername(originUser.getUsername());
        safetyUser.setUserAccount(originUser.getUserAccount());
        safetyUser.setAvatarUrl(originUser.getAvatarUrl());
        safetyUser.setGender(originUser.getGender());
        safetyUser.setPhone(originUser.getPhone());
        safetyUser.setEmail(originUser.getEmail());
        safetyUser.setPlanetCode(originUser.getPlanetCode());
        safetyUser.setUserRole(originUser.getUserRole());
        safetyUser.setUserStatus(originUser.getUserStatus());
        safetyUser.setCreateTime(originUser.getCreateTime());
        safetyUser.setTags(originUser.getTags());
        return safetyUser;
    }

    /**
     * 用户注销
     *
     * @param request
     */
    @Override
    public int userLogout(HttpServletRequest request) {
        // 移除登录态
        request.getSession().removeAttribute(USER_LOGIN_STATE);
        return 1;
    }

    /**
     * 根据用户标签查询用户集合 返回结果List<USER>
     * (数据库查询)
     *
     * @param tagNameList : 用户标签集合 mysql字段设计的是varchar(1024) ["c++","c#"] json数据格式-数组
     * @return
     */
    @Override
    public List<User> searchUsersByTags(List<String> tagNameList) {
        //如果请求参数为空,throw异常->异常处理
        if (CollectionUtils.isEmpty(tagNameList)) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        //数据不为空,查询数据库匹配的用户集合
        //todo 1、根据用户标签查询用户集合 (数据库查询)->finished
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        for (String json : tagNameList) {
            queryWrapper = queryWrapper.like("tags", json); //查询一次 更新一次queryWrapper
        }
        //查询数据库
        List<User> userList = userMapper.selectList(queryWrapper);
        //用户脱敏
        userList = userList.stream().map(item -> {
            return this.getSafetyUser(item);
        }).collect(Collectors.toList());
        return userList;
    }

    /**
     * 根据用户标签查询用户集合 返回结果List<USER>
     * (内存查询)
     *
     * @param tagNameList : 用户标签集合 mysql字段设计的是varchar(1024) ["c++","c#"] json数据格式-数组
     * @return
     */
    @Override
    public List<User> searchUsersByTags2(List<String> tagNameList) {
        //如果请求参数为空,throw异常->异常处理
        if (CollectionUtils.isEmpty(tagNameList)) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        //数据不为空,查询数据库匹配的用户集合
        //todo 2、根据用户标签查询用户集合 (内存查询)->finished
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        //从数据库中查询 User 表的所有记录
        List<User> userList = userMapper.selectList(queryWrapper);
        Gson gson = new Gson();
        //在内存中判断是否包含要求的标签
        return userList.stream().filter(user -> {
            String tags = user.getTags(); //["c++","c#"] json数据格式-数组
            //将tags转为字符串
            Set<String> tempTagNameSet = gson.fromJson(tags, new TypeToken<Set<String>>() {
            }.getType());
            /**
             * 这段代码的主要目的是避免 NullPointerException。
             * 当不确定 tempTagNameSet 是否为 null 时，
             * 使用 Optional.ofNullable(tempTagNameSet).orElse(new HashSet<>())
             * 可以确保 tempTagNameSet 不为 null，方便后续操作。
             */
            tempTagNameSet = Optional.ofNullable(tempTagNameSet).orElse(new HashSet<>());
            //遍历传入的参数是否与数据库查询到的字段匹配
            for (String tagName : tagNameList) {
                if (!tempTagNameSet.contains(tagName)) {
                    //如果数据库这一行数据与传入数据字段不匹配返回false 当前遍历的行数据不要了
                    //这里的逻辑是要用户数据包含全部的查询字段
                    return false;
                }
            }
            return true;
        }).map(this::getSafetyUser).collect(Collectors.toList());
    }

    @Override
    public User getLoginUser(HttpServletRequest request) {
        //获取当当前的用户登录信息
        Object userLogin = request.getSession().getAttribute(USER_LOGIN_STATE);
        //鉴权
        //如果当前用户为空 则无权限
        if (userLogin == null) {
            throw new BusinessException(ErrorCode.NO_AUTH);
        }
        return (User) userLogin;
    }

    /**
     * 更改用户信息
     *
     * @param user      前端传入的要修改的用户信息
     * @param loginUser 当前登录的用户信息
     * @return
     */
    @Override
    public int updateUser(User user, User loginUser) {
        //鉴权
        //拿到前端提供的需要修改的用户id
        long UserId = user.getId();
        //判断用户id是否合法
        if (UserId <= 0) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        //todo 补充校验，如果用户没有传任何要更新的值，就直接报错，不用执行 update 语句->finished
        if (loginUser.equals(user)) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        //如果不是管理员和自己 无权限
        if (!isAdmin(loginUser) && UserId != loginUser.getId()) {
            throw new BusinessException(ErrorCode.NO_AUTH);
        }
        User oldUser = userMapper.selectById(UserId);
        //判断前端要修改的用户是否为空
        if (oldUser == null) {
            throw new BusinessException(ErrorCode.NULL_ERROR);
        }
        //如果是管理员/自己 可以修改
        return userMapper.updateById(user);
    }

    /**
     * 是否为管理员 request获取session
     *
     * @param request
     * @return
     */
    @Override
    public boolean isAdmin(HttpServletRequest request) {
        // 仅管理员可查询
        Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
        User user = (User) userObj;
        return user != null && user.getUserRole() == ADMIN_ROLE;
    }

    /**
     * 判断是否为管理员 获取userRole 字段是否为1
     *
     * @param loginUser
     * @return
     */
    @Override
    public boolean isAdmin(User loginUser) {
        return loginUser != null && loginUser.getUserRole() == ADMIN_ROLE;
    }

    /**
     * 匹配用户接口->优先队列实现
     *
     * @param num:获取推荐用户人数
     * @param loginUser:登录用户
     * @return
     */
    @Override
    public List<User> matchUsersByPriorityQueue(long num, User loginUser) {
        //1、在数据库中先查询所有用户列表并过滤标签列为空的数据
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.select("id", "tags");
        queryWrapper.isNotNull("tags");
        List<User> userList = this.list(queryWrapper);
        //2、获取当前用户的标签备用 用作匹配->转换为字符数组
        String tags = loginUser.getTags();
        Gson gson = new Gson();
        List<String> tagList = gson.fromJson(tags, new TypeToken<List<String>>() {
        }.getType());
        //3、使用优先队列来减少Top N运算过程中的内存占用：用优先队列维护固定长度的有序集合,按照分数从小到大排序
        //保证 最小堆的特性 维护固定长度的有序集合:num个
        User maxScoreUser = null; //记录队列中分数最大的元素
        PriorityQueue<User> topUsers = new PriorityQueue<>(new Comparator<User>() {
            @Override
            public int compare(User user1, User user2) {
                //<----------------
                //10 20 30
                //<----------------
                return Long.compare(user1.getScore(), user2.getScore());
            }
        });
        //4、计算当前登录用户和列表用户的相似度 并保存在topUsers优先队列中
        for (User user : userList) {
            //过滤空标签和当前用户
            if (StringUtils.isBlank(user.getTags()) || Objects.equals(user.getId(), loginUser.getId())) {
                continue;
            }
            //将每一个用户标签字段解析
            String userTags = user.getTags();
            //将用户标签json字段解析
            List<String> tagListUser = gson.fromJson(userTags, new TypeToken<List<String>>() {
            }.getType());
            //计算匹配分数 转为long类型
            long score = AlgorithmUtils.minDistance(tagList, tagListUser);
            //如果队列未满或 当前用户分数更低,添加或者替换
            if (topUsers.size() < num || score < topUsers.peek().getScore()) {
                //队列未满逻辑直添加到队列
                topUsers.add(user);
                //更新最大分数元素User对象
                if (maxScoreUser == null || user.getScore() > maxScoreUser.getScore()) {
                    maxScoreUser = user;
                }
                //如果队伍满了但是当前用户分数小于队列头用户分数将队列中分数最大的踢出队列
                if (topUsers.size() >= num && score < topUsers.peek().getScore()) {
                    topUsers.remove(maxScoreUser);
                }
            }
        }
        //5、获取队列用户的id列表->升序的id集合列表
        ArrayList<Long> userIds = new ArrayList<>();
        //遍历队列
        while (!topUsers.isEmpty()) {
            userIds.add(topUsers.poll().getId());
        }
        //6、在数据库中查询id列表对应的用户体
        QueryWrapper<User> queryWrapperUser = new QueryWrapper<>();
        queryWrapperUser.in("id", userIds);
        //查询到的是乱序的user
        List<User> resultUser = this.list(queryWrapperUser);
        //返回有序的user完整信息
        Map<Long, List<User>> collect = resultUser.stream().map(user2 -> {
            //脱敏
            return getSafetyUser(user2);
        }).collect(Collectors.groupingBy(ooo -> {
            return ooo.getId();
        }));
        //7、返回排序好的User集合
        List<User> finalUserList = new ArrayList<>();
        for (Long userId : userIds) {
            finalUserList.add(collect.get(userId).get(0));
        }
        return finalUserList;
    }

    /**
     * 匹配用户接口
     *
     * @param num:获取推荐用户人数
     * @param loginUser:登录用户
     * @return
     */
    @Override
    public List<User> matchUsers(long num, User loginUser) {
        //1、查询用户列表 注意查询字段
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.select("tags", "id");
        //排除掉tags字段为空的数据行
        queryWrapper.isNotNull("tags");
        //查询user表->得到User用户数据
        List<User> userList = this.list(queryWrapper);
        //2、登录用户的标签字段(json数据解析)
        String tags = loginUser.getTags();
        Gson gson = new Gson();
        List<String> tagList = gson.fromJson(tags, new TypeToken<List<String>>() {
        }.getType());
        //3、创建一个Pair集合 维护计算匹配用户和分数(用户=>相似度)
        List<Pair<User, Long>> userAndScoreList = new ArrayList<>();
        //4、计算当前登录用户和列表用户的相似度 并保存在userAndScoreList集合中
        for (User user : userList) {
            String userTags = user.getTags();
            //过滤空标签和自己
            if (user.getId() == loginUser.getId() || StringUtils.isBlank(user.getTags())) {
                continue;
            }
            //将用户标签json字段解析
            List<String> tagListUser = gson.fromJson(userTags, new TypeToken<List<String>>() {
            }.getType());
            //计算匹配分数 转为long类型
            long score = AlgorithmUtils.minDistance(tagList, tagListUser);
            //保存到Pair集合中
            userAndScoreList.add(new Pair<User, Long>(user, score));
        }
        //5、将Pair集合中的推荐用户根据score排序(从小到大排序 升序)
        /**
         List<User> userListSorted = userAndScoreList.stream()
         .sorted((pair1, pair2) -> {
         return (int) (pair1.getValue() - pair2.getValue());
         })
         .limit(num)
         .map(item -> {
         User safetyUser = getSafetyUser(item.getKey());
         return safetyUser;
         }).collect(Collectors.toList());
         */
        List<Pair<User, Long>> topUserPairList = userAndScoreList.stream().sorted((pair1, pair2) -> {
            return (int) (pair1.getValue() - pair2.getValue());
        }).limit(num)
                .collect(Collectors.toList());
        //取出有顺序的userId列表
        List<Long> userListVo = topUserPairList.stream().map(pair -> {
            return pair.getKey().getId();
        //[3, 4, 5, 6, 7, 8, 9, 10, 2]
        }).collect(Collectors.toList());
        return getFinalUsersBySql(userListVo);
    }

    /**
     * 获得rediskey
     * @param key
     * @return
     */
    @Override
    public String redisFormat(Long key) {
        return String.format("jingSai:user:search:%s", key);
    }

    /**
     * 根据id顺序集合获取用户组（内存版）
     *
     * @param userListVo 带顺序的id集合
     * @return 用户组
     */
    private List<User> getFinalUsersByMemory(List<Long> userListVo) {
        //根据id查询user完整信息
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.in("id", userListVo);
        //取出带顺序id列表的用户信息
        List<User> userList = this.list(queryWrapper);
        //脱敏用户 + 根据用户id分组
        Map<Long, List<User>> userIdUserListMap = userList.stream().map(item -> {
            return getSafetyUser(item);
        }).collect(Collectors.groupingBy(user -> {
            return user.getId();
        }));
        //因为上面查询打乱了顺序,这里根据参数的有序id列表赋值给finalUserList
        List<User> finalUserList = new ArrayList<>();
        for (Long userId : userListVo) {
            finalUserList.add(userIdUserListMap.get(userId).get(0));
        }
        return finalUserList;
    }

    /**
     * 根据id顺序集合获取用户组（sql版）
     *
     * @param userListVo 带顺序的id集合
     * @return 用户组
     */
    private List<User> getFinalUsersBySql(List<Long> userListVo) {
        QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
        //使用sql拼接实现list user in userListVo orderByField userListVO
        userQueryWrapper.in("id", userListVo);
        userQueryWrapper.last("order by field(id," + StringUtils.join(userListVo, ",") + ")");//StringUtils.join(userListVo, ",")将userListVo集合中的元素用逗号拼接成一个字符串 order by field 是mysql中的一盒函数
        return this.list(userQueryWrapper).stream().map(this::getSafetyUser).collect(Collectors.toList());
    }
}
